﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

namespace Stauffware.Common.Core
{
	/// <summary>
	/// A reusable lexer for numbers.
	/// </summary>
	/// <remarks>
	/// Each NumberLexer object is based on a set of numeric formatting parameters that 
	/// are fixed for the life of the NumberLexer object.  
	/// Each Lexer object can be used repeatedly to lex different input strings.
	/// NumberLexer does *NOT* handle conversion to a numeric type (e.g. int, float).
	/// 
	/// To use a NumberLexer, call LexNumber and pass it the string containing the
	/// number to be lexed.  LexNumber returns a boolean indicating whether a number
	/// was successfully lexed.  All other output information is placed in public
	/// fields of the NumberLexer object.
	/// 
	///	Lexical Grammar:
	///
	///	{number}	:=	[{sign} {white}] {unsigned}
	///				 |  {unsigned} [{white} {sign}]
	///				 |  '(' {white} {unsigned} {white} ')'
	///	{unsigned}	:=	{mantissa} [{white} {exponent}] 
	///	{mantissa}	:=	({digit}... ['.' [{digit}...]])  |  ('.' {digit}...)
	///	{exponent}	:=	('e' | 'E') {white} [{sign} {white}] {digit}...
	///	{white}		:=  zero or more whitespace characters
	///	
	/// </remarks>

	public class NumberLexer
	{
		//-----------------------------------------------------------------------------
		// outputs (public)

		/// <summary>
		/// Output: the number of leading zeros left of the decimal point.
		/// </summary>
		public int LeadingZeros;

		/// <summary>
		/// Output: the digits left of the decimal point, 
		/// without grouping characters or extraneous leading zeros.
		/// This may be empty, but never null.
		/// </summary>
		public string DigitsLeft;

		/// <summary>
		/// Output: the sign character, or '\0' if there wasn't one.
		/// </summary>
		public char SignChar;

		/// <summary>
		/// Output: is the number negative?
		/// </summary>
		public bool IsNegative;

		/// <summary>
		/// Output: the digits right of the decimal point, 
		/// without grouping characters or trailing zeros.
		/// This may be empty, but never null.
		/// </summary>
		public string DigitsRight;

		/// <summary>
		/// Output: the number of trailing zeros right of the decimal point.
		/// </summary>
		public int TrailingZeros;

		/// <summary>
		/// Output: the sign character for the exponent, or '\0' if there wasn't one.
		/// </summary>
		public char ExponentSignChar;

		/// <summary>
		/// Output: is the exponent negative?
		/// </summary>
		public bool ExponentIsNegative;

		/// <summary>
		/// Output: the exponent digits, without extraneous leading zeros.
		/// Null if no exponent was found.
		/// </summary>
		public string ExponentDigits;

		/// <summary>
		/// Output: the number of characters lexed from the input string.
		/// </summary>
		public int CharsLexed;

		/// <summary>
		/// Output: whether the input string contained a decimal point.
		/// </summary>
		public bool HasDecimalPoint;

		/// <summary>
		/// Output: whether the input string contained one or more digit separators.
		/// </summary>
		public bool HasSeparators;

		/// <summary>
		/// Output: an error messaage, or null if no error was found.
		/// </summary>
		public string ErrorMessage;

		//-----------------------------------------------------------------------------

		private string Input;
		private bool Dirty;		// indicates that the Lexer has been used since last Init()

		//-----------------------------------------------------------------------------
		// configuration params

		private readonly int MaxDigits;
		private readonly int MaxExponentDigits;
		private readonly NumberParseOptions Options;
		private readonly NumberFormatInfo FormatInfo;

		private static readonly string NegativeSigns = "-" + Chars.MinusSign;
		private const char PositiveSign = '+';
		private const char DecimalPoint = '.';
		private const char DigitSeparator = ',';
		private const int DigitGroupSize = 3;
		private const char ExponentMarkerLC = 'e';

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="options">options to be used for lexing numbers</param>
		/// <param name="fmtInfo">formatting info to be used for lexing numbers</param>
		/// <param name="maxDigits">the maximum number of digits allowed</param>
		/// <param name="maxExponentDigits">the maximum number of exponent digits allowed</param>

		public NumberLexer (NumberParseOptions options, NumberFormatInfo fmtInfo, int maxDigits, int maxExponentDigits)
		{
			this.MaxDigits = maxDigits;
			this.MaxExponentDigits = maxExponentDigits;
			this.Options = options;
			this.FormatInfo = fmtInfo;
			this.Init();


			// TO-DO: get needed info from NumberFormatInfo

		}

		//-----------------------------------------------------------------------------

		private void Init ()
		{
			this.CharsLexed = 0;
			this.DigitsLeft = "";
			this.DigitsRight = "";
			this.Dirty = false;
			this.ErrorMessage = null;
			this.ExponentDigits = null;
			this.ExponentIsNegative = false;
			this.ExponentSignChar = '\0';
			this.HasDecimalPoint = false;
			this.HasSeparators = false;
			this.IsNegative = false;
			this.LeadingZeros = 0;
			this.SignChar = '\0';
			this.TrailingZeros = 0;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Attempts to lex a number from the beginning of the given input string.
		/// </summary>
		/// <param name="input">the input to be lexed</param>
		/// <returns>true if a number was successfully lexed; otherwise false</returns>

		public bool LexNumber (string input)
		{
			Require.NotNull(input);

			if (this.Dirty)
				this.Init();
			this.Dirty = true;
			this.Input = input;

			this.SkipWhitespaceIf(this.Options.HasFlag(NumberParseOptions.AllowLeadingWhite));

			if (this.NoMoreInput(null))
				return false;

			// look for a leading sign or open parenthesis

			//bool foundOpenParen = false;
			//if (styles.HasFlag(NumberStyles.AllowParentheses) && this.Input[this.Next] == '(')
			//{
			//    this.Next++;
			//    foundOpenParen = true;
			//}

			bool foundLeadingSign = false;
			if (this.Options.HasFlag(NumberParseOptions.AllowLeadingSign))
				foundLeadingSign = this.LexSign(ref this.SignChar, ref this.IsNegative);

			// look for a mantissa

			this.LexDigitsLeft(out this.LeadingZeros, out this.DigitsLeft);
			if (this.ErrorMessage != null)
				return false;

			if (this.NoMoreInput(null))
				return (this.DigitsLeft.Length > 0);

			if (this.Input[this.CharsLexed] == DecimalPoint && 
				this.Options.HasFlag(NumberParseOptions.AllowDecimalPoint))
			{
				this.HasDecimalPoint = true;
				this.CharsLexed++;
				this.LexDigitsRight(out this.DigitsRight, out this.TrailingZeros);
				if (this.ErrorMessage != null)
					return false;

				if (this.NoMoreInput(null))
					return (this.DigitsLeft.Length + this.DigitsRight.Length) > 0;
			}

			if (this.Options.HasFlag(NumberParseOptions.AllowExponent))
				this.LexExponent();

			if (this.Options.HasFlag(NumberParseOptions.AllowTrailingSign) && !foundLeadingSign)
				this.LexSign(ref this.SignChar, ref this.IsNegative);

			if (this.Options.HasFlag(NumberParseOptions.ParseEntireInput))
			{
				this.SkipWhitespaceIf(this.Options.HasFlag(NumberParseOptions.AllowTrailingWhite));

				if (this.CharsLexed < this.Input.Length)
				{
					this.CharsLexed++;
					this.ErrorMessage = "invalid character in number";
					return false;
				}
			}

			return (this.DigitsLeft.Length > 0);
		}

		//-----------------------------------------------------------------------------
		// lexes digits and separators left of the decimal point (if any)

		private void LexDigitsLeft (out int leadingZeroes, out string digits)
		{
			int start = this.CharsLexed;

			// skip leading zeroes
			for (; this.CharsLexed < this.Input.Length; this.CharsLexed++)
				if (this.Input[this.CharsLexed] != '0')
					break;

			leadingZeroes = this.CharsLexed - start;
			digits = null;

			char[] digitChars = new char[this.MaxDigits];
			int digitCount = 0;
			int currentGroupSize = 0;

			for ( ; this.CharsLexed < this.Input.Length; this.CharsLexed++)
			{
				if (char.IsDigit(this.Input[this.CharsLexed]))
				{
					if (digitCount >= this.MaxDigits)
					{
						this.ErrorMessage = "too many digits";
						return;
					}

					digitChars[digitCount++] = this.Input[this.CharsLexed];
					currentGroupSize++;
					continue;
				}

				if (!this.Options.HasFlag(NumberParseOptions.AllowGrouping))
					break;
				if (this.Input[this.CharsLexed] != DigitSeparator)
					break;

				// we found a digit separator; check size of digit groups

				if ((currentGroupSize > DigitGroupSize) || (currentGroupSize == 0) ||	
					(currentGroupSize < DigitGroupSize && this.HasSeparators))		// special case for 1st group
				{
					this.ErrorMessage = "invalid digit grouping";
					return;
				}

				currentGroupSize = 0;
			}

			// check size of final digit group, if any
			if (this.HasSeparators && (currentGroupSize != DigitGroupSize))
			{
				this.ErrorMessage = "invalid digit grouping";
				return;
			}

			if (digitCount == 0 && leadingZeroes > 0)	// special case for zero
			{
				digits = "0";
				leadingZeroes--;
			}
			else digits = new string(digitChars, 0, digitCount);
		}

		//-----------------------------------------------------------------------------
		// lexes digits right of the decimal point (if any)

		private void LexDigitsRight (out string digits, out int trailingZeros)
		{
			char[] digitChars = new char[this.MaxDigits];
			int digitCount = 0;
			digits = null;
			trailingZeros = 0;

			for ( ; this.CharsLexed < this.Input.Length; this.CharsLexed++)
			{
				if (!char.IsDigit(this.Input[this.CharsLexed]))
					break;

				if (digitCount >= this.MaxDigits)
				{
					this.ErrorMessage = "too many digits";
					return;
				}

				digitChars[digitCount++] = this.Input[this.CharsLexed];
			}

			int lastNonZero;
			for (lastNonZero = digitCount - 1; lastNonZero >= 0; lastNonZero--)
				if (digitChars[lastNonZero] != '0')
					break;

			lastNonZero++;
			trailingZeros = digitCount - lastNonZero;
			digits = new string(digitChars, 0, lastNonZero);
		}

		//-----------------------------------------------------------------------------
		// lexes an exponent; returns whether one was found

		private bool LexExponent ()
		{
			if (char.ToLower(this.Input[this.CharsLexed]) != ExponentMarkerLC)
				return false;
			this.CharsLexed++;

			this.LexSign(ref this.ExponentSignChar, ref this.ExponentIsNegative);

			int start = this.CharsLexed;
			int digitCount = 0;

			for ( ; this.CharsLexed < this.Input.Length; this.CharsLexed++)
			{
				if (!char.IsDigit(this.Input[this.CharsLexed]))
					break;
				digitCount++;
				if (digitCount >= MaxExponentDigits)
				{
					this.ErrorMessage = "too many digits in exponent";
					return false;
				}
			}

			if (digitCount == 0)
			{
				this.ErrorMessage = "expecting signed integer after '" + this.Input[start-1] + "'";
				return false;
			}

			this.ExponentDigits = this.Input.Substring(start, this.CharsLexed - start);
			return true;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Lexes a positive or negative sign.
		/// </summary>
		/// <param name="sign">if a sign character is found, this is set to the sign character</param>
		/// <param name="isNegative">if a sign character is found, this is set to whether the sign is negative</param>
		/// <returns>whether a sign character was found</returns>

		private bool LexSign (ref char sign, ref bool isNegative)
		{
			int signPos = NegativeSigns.IndexOf(this.Input[this.CharsLexed]);
			
			if (signPos >= 0)
			{
				isNegative = true;
				sign = NegativeSigns[signPos];
				this.CharsLexed++;
				return true;
			}
			
			if (this.Input[this.CharsLexed] == PositiveSign)
			{
				isNegative = false;
				sign = PositiveSign;
				this.CharsLexed++;
				return true;
			}

			return false;
		}

		//-----------------------------------------------------------------------------

		//private bool LexSignOr (char parenthesis, out bool isNegative)
		//{
		//    isNegative = false;	// til proven otherwise

		//    if (NegativeSigns.IndexOf(this.Input[this.CharsLexed]) >= 0)
		//    {
		//        isNegative = true;
		//        this.CharsLexed++;
		//        return true;
		//    }
		//    else if (this.Input[this.CharsLexed] == PositiveSign)
		//    {
		//        this.CharsLexed++;
		//        return true;
		//    }
		//    else if (parenthesis != '\0' && this.Input[this.CharsLexed] == parenthesis)
		//    {
		//        this.CharsLexed++;
		//        return true;
		//    }

		//    return false;
		//}

		//-----------------------------------------------------------------------------

		//private bool LexAnyOf (out char found, params char[] characters)
		//{
		//    if (this.CharsLexed < this.Input.Length)
		//    {
		//        foreach (char c in characters)
		//            if (this.Input[this.CharsLexed] == c)
		//            {
		//                this.CharsLexed++;
		//                found = c;
		//                return true;
		//            }
		//    }

		//    found = '\0';
		//    return false;
		//}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Skips leading whitespace if the flag is true.
		/// </summary>

		private void SkipWhitespaceIf (bool flag)
		{
			if (!flag) return;
			for (; this.CharsLexed < this.Input.Length; this.CharsLexed++)
				if (!char.IsWhiteSpace(this.Input[this.CharsLexed]))
					break;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Checks to see if we've run out of input.
		/// </summary>
		/// <param name="errMsg">assigned to this.ErrorMessage if we've run out of input</param>
		/// <returns>true if we've run out, false if there's still more</returns>

		private bool NoMoreInput (string errMsg)
		{
			if (this.CharsLexed < this.Input.Length)
				return false;	// there's still more

			this.ErrorMessage = errMsg;
			return true;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Returns the number of decimal digits at the beginning of the given string.
		/// </summary>
		/// <param name="input">the string to examine</param>
		/// <param name="start">where in the string to start looking</param>
		/// <returns>the number of digits</returns>

		public static int LexDigits (string input, int start)
		{
			Require.NotNull(input);
			Require.Condition(start <= input.Length);

			int n;
			for (n = start; n < input.Length; n++)
				if (!char.IsDigit(input[n]))
					break;
			return n - start;
		}

		//-----------------------------------------------------------------------------
	}
}
